home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
Shareware Grab Bag
/
Shareware Grab Bag.iso
/
011
/
tsrsrc22.arc
/
RELEASE.PAS
< prev
next >
Wrap
Pascal/Delphi Source File
|
1987-03-05
|
29KB
|
824 lines
{**************************************************************************
* Releases memory above the last MARK call made. *
* Copyright (c) 1986 Kim Kokkonen, TurboPower Software. *
* Released to the public domain for personal, non-commercial use only. *
***************************************************************************
* Version 1.0 2/8/86 *
* original public release. *
* (thanks to Neil Rubenking for an outline of the method used) *
* Version 1.1 2/11/86 *
* fixed problem with processes which deallocate their environment. *
* Version 1.2 2/13/86 *
* fixed another problem with processes which deallocate environment. *
* Version 1.3 2/15/86 *
* added support for "named" marks. *
* Version 1.4 2/23/86 *
* added support for releasing programs which use Expanded Memory. *
* Version 1.5 2/28/86 *
* added more bulletproof method of finding first allocation block. *
* Version 1.6 3/20/86 *
* restore all FF interrupts. *
* restore the termination address to the local process. *
* reduce number of EMS blocks to 32. *
* fix bug in number of EMS handles in EMS release step. *
* restore an undocumented address in the PSP which allows RELEASE of *
* a COMMAND shell (emulates the EXIT command). *
* Version 1.7 (date not recorded) *
* add "protected" marks. *
* Version 1.8 4/21/86 *
* fix problem when mark is installed as 'MARK '. *
* Version 1.9 5/22/86 *
* release the environment of MARK when it is not contiguous with *
* the MARK itself. *
* capture RELEASE calls from within batch files and don't release the *
* batch control block. *
* fiddle with different methods of restoring interrupt vectors in *
* an attempt to successfully remove DoubleDos. No success, not *
* implemented. Note, after more effort: DDos apparently *
* reprograms the 8259 as well as patching the operating system. *
* Version 2.0 6/17/86 *
* support "file" marks placed by the new program FMARK. *
* Version 2.1 7/18/86 *
* fix bug in restoring "parent" address in RELEASE PSP. *
* Version 2.2 3/3/87 *
* add option to revector 8259 interrupt controller. *
* (thanks to Steve Glynn for this code) *
* add option to leave mark in place when RELEASE is run. *
* restore save areas for EGA and interapplication communications *
* *
***************************************************************************
* telephone: 408-438-8608, CompuServe: 72457,2131. *
* requires Turbo version 3 to compile. *
* Compile with mAx dynamic memory = FFFF. *
***************************************************************************}
{$P128}
{$C-}
program ReleaseTSR;
{-release system memory above the last mark call}
{-release expanded memory blocks allocated since the last mark call}
const
Version = '2.2';
ProtectChar = '!'; {marks whose name begins with this will be
released ONLY if an exact name match occurs}
MaxBlocks = 128; {max number of DOS allocation blocks supported}
MaxHandles = 32; {max number of EMS allocation blocks supported}
EMSinterrupt = $67; {the vector used by the expanded memory manager}
MarkID = 'MARK PARAMETER BLOCK FOLLOWS'; {marking string for TSR MARK}
FmarkID = 'FMARK TSR'; {marking string for TSR FMARK}
{offsets into resident copy of MARK.COM for data storage}
MarkOffset = $103; {where markID is found in MARK TSR}
FmarkOffset = $60; {where fmarkID is found in FMARK TSR}
VectorOffset = $120; {where vector table is stored}
EGAsavOffset = $520; {where the EGA save save is stored}
IntComOffset = $528; {where the interapps comm area is stored}
EMScntOffset = $538; {where count of EMS active pages is stored}
EMSmapOffset = $53A; {where the page map is stored}
type
registers =
record case Integer of
1 : (ax, bx, cx, dx, bp, si, di, ds, es, flags : Integer);
2 : (al, ah, bl, bh, cl, ch, dl, dh : Byte);
end;
HandlePageRecord =
record
handle : Integer;
numpages : Integer;
end;
PageArray = array[1..MaxHandles] of HandlePageRecord;
PageArrayPtr = ^PageArray;
Block =
record {store info about each memory block}
mcb : Integer;
psp : Integer;
releaseIt : Boolean;
end;
BlockType = 0..MaxBlocks;
BlockArray = array[BlockType] of Block;
AllStrings = string[255];
HexString = string[4];
var
Blocks : BlockArray;
bottomBlock, blockNum : BlockType;
markName : AllStrings;
Regs : registers;
FilMarkHandles, ReturnCode, StartMCB, StoredHandles, EMShandles : Integer;
FilMarkPageMap, Map, StoredMap : PageArrayPtr;
TrappedBytes : Real;
Debug, Revector8259, KeepMark, MemMark, FilMark : Boolean;
Vectors : array[0..1023] of Byte;
EGAsavTable : array[0..7] of byte;
IntComTable : array[0..15] of byte;
procedure Abort(msg : AllStrings);
{-halt in case of error}
begin
WriteLn(msg);
Halt(1);
end {Abort} ;
procedure FindTheBlocks;
{-scan memory for the allocated memory blocks}
const
MidBlockID = $4D; {byte DOS uses to identify part of MCB chain}
EndBlockID = $5A; {byte DOS uses to identify last block of MCB chain}
var
mcbSeg : Integer; {segment address of current MCB}
nextSeg : Integer; {computed segment address for the next MCB}
gotFirst : Boolean; {true after first MCB is found}
gotLast : Boolean; {true after last MCB is found}
idbyte : Byte; {byte that DOS uses to identify an MCB}
function GetStartMCB : Integer;
{-return the first MCB segment}
begin
Regs.ah := $52;
MsDos(Regs);
GetStartMCB := MemW[Regs.es:(Regs.bx-2)];
end {getstartmcb} ;
procedure StoreTheBlock(var mcbSeg, nextSeg : Integer;
var gotFirst, gotLast : Boolean);
{-store information regarding the memory block}
var
nextID : Byte;
pspAdd : Integer; {segment address of the current PSP}
mcbLen : Integer; {size of the current memory block in paragraphs}
begin
mcbLen := MemW[mcbSeg:3]; {size of the MCB in paragraphs}
nextSeg := Succ(mcbSeg+mcbLen); {where the next MCB should be}
pspAdd := MemW[mcbSeg:1]; {address of program segment prefix for MCB}
nextID := Mem[nextSeg:0];
if gotLast or (nextID = EndBlockID) or (nextID = MidBlockID) then begin
blockNum := Succ(blockNum);
gotFirst := True;
with Blocks[blockNum] do begin
mcb := mcbSeg;
psp := pspAdd;
end;
end;
end {storetheblock} ;
begin
{initialize}
StartMCB := GetStartMCB;
mcbSeg := StartMCB;
gotFirst := False;
gotLast := False;
blockNum := 0;
{scan all memory until the last block is found}
repeat
idbyte := Mem[mcbSeg:0];
if idbyte = MidBlockID then begin
StoreTheBlock(mcbSeg, nextSeg, gotFirst, gotLast);
if gotFirst then
mcbSeg := nextSeg
else
mcbSeg := Succ(mcbSeg);
end else if gotFirst and (idbyte = EndBlockID) then begin
gotLast := True;
StoreTheBlock(mcbSeg, nextSeg, gotFirst, gotLast);
end else
{start block was invalid}
Abort('Corrupted allocation chain or program error....');
until gotLast;
end {findtheblocks} ;
function StUpcase(s : AllStrings) : AllStrings;
{-return the uppercase string}
var
i : Byte;
begin
for i := 1 to Length(s) do
s[i] := UpCase(s[i]);
StUpcase := s;
end {stupcase} ;
function FindMark(markName : AllStrings) : Integer;
{-find the last memory block matching idstring at offset idoffset}
var
b : BlockType;
function HasIDstring(segment : Integer;
idString : AllStrings;
idOffset : Integer) : Boolean;
{-return true if idstring is found at segment:idoffset}
var
tString : AllStrings;
len : Byte;
begin
len := Length(idString);
tString[0] := Chr(len);
Move(Mem[segment:idOffset], tString[1], len);
HasIDstring := (tString = idString);
end {HasIDstring} ;
function GetMarkName(segment : Integer) : AllStrings;
{-return a cleaned up mark name from the segment's PSP}
var
tString : AllStrings;
tlen : Byte absolute tString;
begin
Move(Mem[segment:$80], tString[0], 128);
while (tlen > 0) and ((tString[1] = ' ') or (tString[1] = ^I)) do
Delete(tString, 1, 1);
while (tlen > 0) and ((tString[tlen] = ' ') or (tString[tlen] = ^I)) do
tlen := Pred(tlen);
GetMarkName := StUpcase(tString);
end; {GetMarkName}
function MatchMemMark(segment : Integer;
markName : AllStrings;
var b : BlockType) : Boolean;
{-return true if MemMark is unnamed or matches current name}
var
tString : AllStrings;
FoundIt : Boolean;
begin
{check the mark name stored in the PSP of the mark block}
tString := GetMarkName(segment);
if (markName <> '') then begin
FoundIt := (tString = StUpcase(markName));
if not(FoundIt) then
if (tString <> '') and (tString[1] = ProtectChar) then
{current mark is protected, stop searching}
b := 1;
end else if (tString <> '') and (tString[1] = ProtectChar) then begin
{stored mark name is protected}
FoundIt := False;
{stop checking}
b := 1;
end else
{match any mark}
FoundIt := True;
if not(FoundIt) then
b := Pred(b);
MatchMemMark := FoundIt;
end {MatchMemMark} ;
function MatchFilMark(segment : Integer;
markName : AllStrings;
var b : BlockType) : Boolean;
{-return true if FilMark is unnamed or matches current name}
var
tString : AllStrings;
FoundIt : Boolean;
function ExistFile(path : AllStrings) : Boolean;
{-return true if file exists}
var
f : file;
begin
Assign(f, path);
{$I-}
Reset(f);
{$I+}
ExistFile := (IOResult = 0);
Close(f);
end; {existfile}
begin
{check the mark name stored in the PSP of the mark block}
tString := GetMarkName(segment);
if (markName <> '') then begin
markName := StUpcase(markName);
FoundIt := (tString = markName);
if FoundIt then begin
{Assure named file exists}
WriteLn('Finding mark file: ', markName);
FoundIt := ExistFile(markName);
if not(FoundIt) then
{stop checking}
b := 1;
end;
end else
{file marks must be named on RELEASE command line}
FoundIt := False;
if not(FoundIt) then
b := Pred(b);
MatchFilMark := FoundIt;
end {MatchFilMark} ;
begin
{scan from the last block down to find the last MARK TSR}
b := blockNum;
MemMark := False;
FilMark := False;
repeat
if Blocks[b].psp = CSeg then
{assure this program's command line is not matched}
b := Pred(b)
else if HasIDstring(Blocks[b].psp, markID, markOffset) then
{an in-memory mark}
MemMark := MatchMemMark(Blocks[b].psp, markName, b)
else if HasIDstring(Blocks[b].psp, fmarkID, fmarkOffset) then
{a file mark}
FilMark := MatchFilMark(Blocks[b].psp, markName, b)
else
{not a mark}
b := Pred(b);
until (b < 1) or MemMark or FilMark;
if not(MemMark or FilMark) then
Abort('No matching marker found, or protected marker encountered.');
FindMark := b;
end {findmark} ;
function Hex(i : Integer) : HexString;
{-return hex representation of integer}
const
hc : array[0..15] of Char = '0123456789ABCDEF';
var
l, h : Byte;
begin
l := Lo(i); h := Hi(i);
Hex := hc[h shr 4]+hc[h and $F]+hc[l shr 4]+hc[l and $F];
end {hex} ;
procedure ReadMarkFile(markName : AllStrings);
{-read the mark file info into memory}
var
f : file;
begin
Assign(f, markName);
Reset(f, 1);
{read the vector table from the mark file, into a temporary memory area}
BlockRead(f, Vectors, 1024);
{read the BIOS miscellaneous save areas into temporary tables}
blockread(f, EGAsavTable, 8);
blockread(f, IntComTable, 16);
{read the number of EMS handles stored}
BlockRead(f, FilMarkHandles, 2);
{get a page map area and read the page map into it}
GetMem(FilMarkPageMap, 4*FilMarkHandles);
BlockRead(f, FilMarkPageMap^, 4*FilMarkHandles);
Close(f);
if not(keepmark) then
{delete the mark file so it causes no mischief later}
Erase(f);
end {ReadMarkFile} ;
procedure CopyVectors(bottomBlock : BlockType);
{-put interrupt vectors back into table}
var
bottompsp:integer;
procedure Reset8259;
{-Reset the 8259 interrupt controller to its powerup state}
{-Interrupts assumed OFF prior to calling this routine}
function ATmachine : Boolean;
{-return true if machine is AT class}
var
machtype : Byte absolute $FFFF : $000E;
begin
ATmachine := (machtype = $FC);
end {ATmachine} ;
procedure Reset8259PC;
{-Reset the 8259 on a PC class machine}
begin
inline(
$E4/$21/ { in al,$21}
$88/$C4/ { mov ah,al}
$B0/$13/ { mov al,+$13}
$E6/$20/ { out $20,al}
$B0/$08/ { mov al,+$08}
$E6/$21/ { out $21,al}
$B0/$09/ { mov al,+$09}
$E6/$21/ { out $21,al}
$88/$E0/ { mov al,ah}
$E6/$21 { out $21,al}
);
end {Reset8259PC} ;
procedure Reset8259AT;
{-Reset the 8259 interrupt controllers on an AT machine}
begin
inline(
$32/$C0/ { xor al,al }
$E6/$F1/ { out 0f1h,al ; Switch off an 80287 if necessary}
{Set up master 8259 }
$E4/$21/ { in al,21h ; Get current interrupt mask }
$8A/$E0/ { mov ah,al ; save it }
$B0/$11/ { mov al,11h }
$E6/$20/ { out 20h,al }
$EB/$00/ { jmp short $+2 }
$B0/$08/ { mov al,8 ; Set up main interrupt vector number}
$E6/$21/ { out 21h,al }
$EB/$00/ { jmp short $+2 }
$B0/$04/ { mov al,4 }
$E6/$21/ { out 21h,al }
$EB/$00/ { jmp short $+2 }
$B0/$01/ { mov al,1 }
$E6/$21/ { out 21h,al }
$EB/$00/ { jmp short $+2 }
$8A/$C4/ { mov al,ah }
$E6/$21/ { out 21h,al }
{Set up slave 8259 }
$E4/$A1/ { in al,0a1h ; Get current interrupt mask }
$8A/$E0/ { mov ah,al ; save it }
$B0/$11/ { mov al,11h }
$E6/$A0/ { out 0a0h,al }
$EB/$00/ { jmp short $+2 }
$B0/$70/ { mov al,70h }
$E6/$A1/ { out 0a1h,al }
$B0/$02/ { mov al,2 }
$EB/$00/ { jmp short $+2 }
$E6/$A1/ { out 0a1h,al }
$EB/$00/ { jmp short $+2 }
$B0/$01/ { mov al,1 }
$E6/$A1/ { out 0a1h,al }
$EB/$00/ { jmp short $+2 }
$8A/$C4/ { mov al,ah ; Reset previous interrupt state }
$E6/$A1 { out 0a1h,al }
);
end {Reset8259AT} ;
begin
if ATmachine then
Reset8259AT
else
Reset8259PC;
end {Reset8259} ;
begin
{interrupts off}
inline($FA);
{restore the main interrupt vector table and the misc save areas}
if FilMark then begin
Move(Vectors, Mem[0:0], 1024);
Move(EGAsavTable, Mem[$40:$A8], 8);
Move(IntComTable, Mem[$40:$F0], 16);
end else begin
bottompsp:=Blocks[bottomBlock].psp;
Move(Mem[bottompsp:vectorOffset], Mem[0:0], 1024);
Move(Mem[bottompsp:EGAsavOffset], Mem[$40:$A8], 8);
Move(Mem[bottompsp:IntComOffset], Mem[$40:$F0], 16);
end;
{move the old termination/break/error addresses into this program}
Move(Mem[0:$88], Mem[CSeg:$0A], 12);
{restore the "parent address" used by the DOS EXIT command to remove a shell}
Move(Mem[CSeg:$0C], Mem[CSeg:$16], 2);
{reset 8259 if requested}
if Revector8259 then
Reset8259;
{interrupts on}
inline($FB);
end {CopyVectors} ;
procedure MarkBlocks(bottomBlock : BlockType);
{-mark those blocks to be released}
var
b : BlockType;
commandPsp, markPsp : Integer;
ch : Char;
function Cardinal(i : Integer) : Real;
{-return "unsigned integer" in range 0..65535}
begin
if i < 0 then
Cardinal := i+65536.0
else
Cardinal := i;
end {Cardinal} ;
procedure BatchWarning(b : BlockType);
{-warn about the trapping effect of batch files}
var
t : BlockType;
begin
WriteLn('Memory space for TSRs installed prior to batch file');
WriteLn('will not be released until batch file completes.');
WriteLn;
ReturnCode := 1;
{Accumulate number of bytes temporarily trapped}
for t := 1 to b do
if Blocks[t].releaseIt then
TrappedBytes := TrappedBytes+16.0*Cardinal(MemW[Blocks[t].mcb:3]);
end {BatchWarning} ;
begin
commandPsp := Blocks[2].psp;
markPsp := Blocks[bottomBlock].psp;
for b := 1 to blockNum do
with Blocks[b] do
if (b < bottomBlock) then begin
{release any trapped environment block}
if KeepMark then
releaseIt := (psp <> CSeg) and (Cardinal(psp) > Cardinal(markPsp))
else
releaseIt := (psp <> CSeg) and (Cardinal(psp) >= Cardinal(markPsp));
end else if (psp = commandPsp) then begin
{Don't release blocks owned by COMMAND.COM}
releaseIt := False;
BatchWarning(b);
end else if KeepMark then
{release all but RELEASE and the mark}
releaseIt := (psp <> CSeg) and (psp <> markPsp)
else
{release all but RELEASE itself}
releaseIt := (psp <> CSeg);
if debug then begin
for b := 1 to blockNum do with Blocks[b] do
WriteLn(b:3, ' ', Hex(psp), ' ', Hex(mcb), ' ', releaseIt);
Read(Kbd, ch);
end;
end {MarkBlocks} ;
procedure ReleaseMem;
{-release DOS memory marked for release}
var
b : BlockType;
begin
with Regs do
for b := 1 to blockNum do
with Blocks[b] do
if releaseIt then begin
ah := $49;
{the block is always 1 paragraph above the MCB}
es := Succ(mcb);
MsDos(Regs);
if Odd(flags) then begin
WriteLn('Could not release block at segment ', Hex(es));
Abort('Memory may be a mess... Please reboot');
end;
end;
end {releasemem} ;
function EMSpresent : Boolean;
{-return true if EMS memory manager is present}
var
f : file;
begin
{"file handle" defined by the expanded memory manager at installation}
Assign(f, 'EMMXXXX0');
{$I-}
Reset(f);
{$I+}
EMSpresent := (IOResult = 0);
Close(f);
end {EMSpresent} ;
procedure RestoreEMSmap;
{-restore EMS to state at time of mark}
function EMShandlesActive : Integer;
{-return the number of active EMS handles}
begin
Regs.ah := $4B;
Intr(EMSinterrupt, Regs);
if Regs.ah <> 0 then begin
WriteLn('EMS device not responding');
EMShandlesActive := 0;
Exit;
end;
EMShandlesActive := Regs.bx;
end {EMShandlesActive} ;
function GetHandles(bottomBlock : BlockType; EMScntOffset : Integer) : Integer;
{-return the number of handles stored by mark}
var
gh : Integer;
begin
if FilMark then
GetHandles := FilMarkHandles
else begin
Move(Mem[Blocks[bottomBlock].psp:EMScntOffset], gh, 2);
GetHandles := gh;
end;
end {gethandles} ;
function GetStoredMap(bottomBlock : BlockType; EMSmapOffset : Integer) : PageArrayPtr;
{-returns a pointer to the stored page array}
begin
if FilMark then
GetStoredMap := FilMarkPageMap
else
GetStoredMap := Ptr(Blocks[bottomBlock].psp, EMSmapOffset);
end {GetStoredMap} ;
procedure EMSpageMap(var PageMap : PageArray);
{-return an array of the allocated memory blocks}
begin
Regs.ah := $4D;
Regs.es := Seg(PageMap);
Regs.di := Ofs(PageMap);
Regs.bx := 0;
Intr(EMSinterrupt, Regs);
if Regs.ah <> 0 then
WriteLn('EMS device not responding');
end {EMSpageMap} ;
procedure ReleaseEMSblocks(var oldmap, newmap : PageArray);
{-release those EMS blocks allocated since MARK was installed}
var
o, n, nhandle : Integer;
procedure EMSdeallocate(EMShandle : Integer);
{-release the allocated expanded memory}
begin
Regs.ah := $45;
Regs.dx := EMShandle;
Intr(EMSinterrupt, Regs);
if Regs.ah <> 0 then begin
WriteLn('Program error or EMS device not responding');
Abort('EMS memory may be a mess... Please reboot');
end;
end {EMSdeallocate} ;
begin
for n := 1 to EMShandles do begin
{scan all current handles}
nhandle := newmap[n].handle;
if StoredHandles > 0 then begin
{see if current handle matches one stored by MARK}
o := 1;
while (oldmap[o].handle <> nhandle) and (o <= StoredHandles) do
o := Succ(o);
{if not, deallocate the current handle}
if (o > StoredHandles) then
EMSdeallocate(nhandle);
end else
{no handles stored by MARK, deallocate all current handles}
EMSdeallocate(nhandle);
end;
end {releaseEMSblocks} ;
begin
{see how many EMS handles are currently active}
EMShandles := EMShandlesActive;
if EMShandles > MaxHandles then
WriteLn('EMS process count exceeds capacity of RELEASE - no action taken')
else if EMShandles <> 0 then begin
{see how many handles were active when MARK was installed}
StoredHandles := GetHandles(bottomBlock, EMScntOffset);
{get the existing EMS page map}
GetMem(Map, 4*EMShandles);
EMSpageMap(Map^);
{get the stored page map}
StoredMap := GetStoredMap(bottomBlock, EMSmapOffset);
{compare the two maps and deallocate pages not in the stored map}
ReleaseEMSblocks(StoredMap^, Map^);
end;
end {RestoreEMSmap} ;
procedure GetOptions;
{-Analyze command line for options}
var
arg : AllStrings;
arglen : Byte absolute arg;
i : Integer;
procedure WriteHelp;
{-Show the options}
begin
WriteLn('RELEASE ', Version, ', by TurboPower Software');
WriteLn('====================================================');
WriteLn('RELEASE removes memory-resident programs from memory');
WriteLn('and restores the interrupt vectors to their state as');
WriteLn('found prior to the installation of a MARK.');
WriteLn('RELEASE manages both normal DOS memory and also');
WriteLn('Lotus/Intel Expanded Memory.');
WriteLn;
WriteLn('RELEASE accepts the following command line syntax:');
WriteLn;
WriteLn(' RELEASE [Options] [MarkName]');
WriteLn;
WriteLn('Options may be preceded by either / or -. Valid options');
WriteLn('are as follows:');
WriteLn;
WriteLn(' /K Release memory, but Keep the mark in place.');
WriteLn(' /R Revector the 8259 interrupt controller to its');
WriteLn(' powerup state.');
WriteLn(' /? Write this help screen.');
Halt(1);
end {WriteHelp} ;
begin
WriteLn;
{Initialize defaults}
markName := '';
Revector8259 := False;
KeepMark := False;
ReturnCode := 0;
TrappedBytes := 0.0;
Debug := false;
i := 1;
while i <= ParamCount do begin
arg := ParamStr(i);
if (arg[1] = '?') then
WriteHelp
else if (arg[1] = '-') or (arg[1] = '/') then
case arglen of
1 : Abort('Missing command option following '+arg);
2 : case UpCase(arg[2]) of
'?' : WriteHelp;
'R' : Revector8259 := True;
'K' : KeepMark := True;
'D' : Debug := True;
else
Abort('Unknown command option: '+arg);
end;
else
Abort('Unknown command option: '+arg);
end
else
{named mark}
markName := arg;
i := Succ(i);
end;
end {getOptions} ;
begin
{analyze command line for options}
GetOptions;
{get all allocated memory blocks in normal memory}
FindTheBlocks;
{find the last one marked with the MARK idstring, and MarkName if specified}
bottomBlock := FindMark(markName);
{mark those blocks to be released}
MarkBlocks(bottomBlock);
{get file mark information into memory}
if FilMark then
ReadMarkFile(markName);
{copy the vector table from the MARK copy}
CopyVectors(bottomBlock);
{release normal memory marked for release}
ReleaseMem;
{deal with expanded memory}
if EMSpresent then
RestoreEMSmap;
{DOS will release this program's memory when it exits}
{write success message}
Write('RELEASE ', Version, ' - Memory released above last MARK ');
if markName <> '' then
Write('(', StUpcase(markName), ')');
WriteLn;
if ReturnCode <> 0 then
WriteLn(TrappedBytes:0:0, ' bytes temporarily trapped until batch file completes');
Halt(ReturnCode);
end.